
除了在渲染的时候实时计算光照信息,我们还可以在运行前预先计算光源产生的直接照明和间接照明信息,并烘焙(Baked)到光照贴图(Lightmap)或者光照探针(Light Probe)上。当前场景中静态物体表面颜色信息是烘焙到光照贴图,而用于照明动态物体的照明信息是烘焙到光照探针,这样在运行时就不需要重新计算这些照明信息了。间接照明是全局照明的一部分,即光线通过环境或自发光的物体表面照射而来。
5.1.1 场景中光照的设置
若光源的Light组件中Mode属性设为Mixed,我们称之为混合模式光源。在场景中,所有的混合模式光源将使用同一种混合光照模式(Mixed Lighting Mode)。要设置混合光照模式,可以通过Window->Rendering->Lighting Settings调出Lighting窗口,在Scene选项卡中可以看到Mixed Lighting选项,这就是混合光照模式的设置选项。该选项下面有两个子选项,分别是Baked Global Illumin复选框和Lighting Mode下拉列表,下拉列表的子选项有3个,分别是Baked Indirect、Shadowmask和Subtractive。
其中Baked Indirect光照模式仅对光源提供的间接照明部分进行预计算烘焙。本节我们只讨论这个,所以将本场景的混合照明模式设置为Baked Indirect。然后勾选Baked Global Illumin复选框启用烘焙照明的功能。


下图是要生成的光照贴图的常规配置,我们把Lightmap Resolution设置为20,取消Compress Lightmaps勾选,Directional Mode选择 Non-Directional(默认为Directional,它会额外烘焙出一张光源的方向贴图(Directional Map),用来存储物体表面上的入射光方向信息,使得法线贴图可以影响入射烘焙光照,让凹凸感更强一些,因为我们目前还不支持法线贴图,所以不启用它),其它用默认的值就行。

5.1.2 搭建烘焙场景

我们搭建一个用于烘焙的场景,然后主方向光源的Mode属性设置为Mixed,作为混合模式光源,混合使用实时光照和烘焙光照。勾选对象Mesh Renderer组件上的Contribute Global Illumination复选框。对象就可以作为光线反射的对象,提供间接照明,然后Receive Global Illumination属性会自动切换成Lightmaps模式,这意味着到达其表面的间接光也会被烘焙到光照贴图中。做完这个工作后,该对象Inspector面板右上角的Static中的Contribute GI选项就会被勾选,所以你还可以直接勾选Static来更方便地做这个操作。

然后点击Generate Lighting烘焙整个场景的光照信息。


烘焙好以后,我们可以选择一个对象,然后在它的Mesh Renderer组件中的Lightmapping面板中可以看到烘焙的光照贴图的样子,点击Open Preview放大观看,蓝线的矩形就是我们场景中的物体在光照贴图的位置和占用大小,黄色矩形就是你选择的对象所在的位置。烘焙的贴图大部分区域是偏蓝的,是因为天空盒导致的,它代表环境天空的间接照明。
我们把主方向光源的Mode设置为Baked,它会将直接光照和间接光照都烘焙到Lightmap中,然后场景中就不再有实时光照了,会变得很黑。实际上烘焙的直接光照也会被视为间接光照,因此也会出现在Lightmap中,此时Lightmap会显得更加明亮。

现在场景中没有了实时光照,我们需要在着色器中对烘焙好的光照贴图进行采样,从而获取烘焙照明信息。
5.2.1 全局照明(Global Illumination)
1. 在ShaderLibrary文件夹下创建一个GI.hlsl文件,我们将后续所有和全局照明相关的代码写在这里。首先定义一个GI结构体,里面定义一个漫反射颜色,间接光照的来源是不固定的,因此只能用于漫反射照明,其镜面反射一般是通过反射探针实现的。然后定义一个GetGI方法,最开始使用光照贴图的UV来填充漫反射,用于后续调试。
//全局照明相关库
#ifndef CUSTOM_GI_INCLUDED
#define CUSTOM_GI_INCLUDED
struct GI
{
//漫反射颜色
float3 diffuse;
};
GI GetGI(float2 lightMapUV)
{
GI gi;
gi.diffuse = float3(lightMapUV, 0.0);
return gi;
}
#endif
2. 给Lighting.hlsl文件的GetLighting方法添加一个GI结构体的传参,然后用GI的漫反射颜色给color赋一个初始值。这时我们不和表面的漫反射相乘,以便观察未修改的接收光照。
float3 GetLighting(Surface surfaceWS, BRDF brdf, GI gi)
{
...
float3 color = gi.diffuse;
...
}
3. 在LitPass.hlsl文件中把GI.hlsl文件include进来,放在Light.hlsl之前。
#include "../ShaderLibrary/GI.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"
4. 在片元函数中获取调用GetLighting方法之前,我们声明一个GI对象,调用GetGI方法并传递一个值为0的UV坐标,用来获取全局照明数据,然后传递给GetLighting方法。
//获取全局照明数据
GI gi = GetGI(0.0);
float3 color = GetLighting(surface, brdf, gi);
return float4(color, surface.alpha);
5.2.2 光照贴图的UV坐标
1. 要获取光照贴图的UV坐标,需要由Unity将其发送到着色器中,我们需要指示渲染管线对每个被烘焙了光照信息的对象都这样做。在CameraRenderer脚本的DrawVisibleGeometry方法中创建drawingSettings实例时,给每个对象的数据属性设置为PerObjectData.Lightmaps。
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
{
enableDynamicBatching = useDynamicBatching,
enableInstancing = useGPUInstancing,
perObjectData = PerObjectData.Lightmaps
};
2. 在Lit.shader的CustomLit Pass中添加一个带LIGHTMAP_ON关键字的编译指令。它是否启用决定了是否渲染光照贴图对象。
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile_instancing
3. 光照贴图的UV坐标是顶点数据的一部分,应该在顶点和片元输入结构体中都定义它,在顶点函数中将其转换到片元函数中用于贴图采样。但应该只有LIGHTMAP_ON关键字启用时定义它们才有意义,所以我们暂时添加三个宏:GI_ATTRIBUTE_DATA,GI_VARYINGS_DATA和TRANSFER_GI_DATA来作为上面的定义结果,后面会定义这些宏的内容。然后,还需添加一个宏GI_FRAGMENT_DATA,用来检索GetGI方法需要的参数。
struct Attributes
{
...
GI_ATTRIBUTE_DATA
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
...
GI_VARYINGS_DATA
UNITY_VERTEX_INPUT_INSTANCE_ID
};
//顶点函数
Varyings LitPassVertex (Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
TRANSFER_GI_DATA(input, output);
...
}
//片元函数
float4 LitPassFragment(Varyings input) : SV_TARGET
{
...
GI gi = GetGI(GI_FRAGMENT_DATA(input));
...
}
4. 我们在GI.hlsl中定义这些宏,当关键字LIGHTMAP_ON被定义时,应获取lightMap的UV坐标,这是通过第二个纹理坐标通道提供的,然后进行相关转换。当关键字未被定义时,这几个宏都应定义为空,GI_FRAGMENT_DATA要设置为0。
//当需要渲染光照贴图对象时
#if defined(LIGHTMAP_ON)
#define GI_ATTRIBUTE_DATA float2 lightMapUV : TEXCOORD1;
#define GI_VARYINGS_DATA float2 lightMapUV : VAR_LIGHT_MAP_UV;
#define TRANSFER_GI_DATA(input, output) output.lightMapUV = input.lightMapUV;
#define GI_FRAGMENT_DATA(input) input.lightMapUV
#else
//否则这些宏都应为空
#define GI_ATTRIBUTE_DATA
#define GI_VARYINGS_DATA
#define TRANSFER_GI_DATA(input, output)
#define GI_FRAGMENT_DATA(input) 0.0
#endif

光照贴图的UV坐标通常由Unity给每个Mesh自动生成,或者在建模软件中设置好后作为Mesh数据的一部分导入进来,它们会将Mesh平铺展开,像一张纹理一样,且保持不重叠、不拉伸、不旋转,以便将其映射到纹理坐标。然后使所有物体均匀且不重叠的按照缩放和偏移放置在这张光照贴图中,就像将缩放和偏移应用到Base UV一样,我们也将其应用到光照贴图的UV中。
5. 在UnityInput.hlsl文件的UnityPerDraw缓冲区中我们定义光照贴图和动态光照贴图的UV转换属性。添加动态光照贴图的UV转换属性是为了防止因为一些兼容问题而导致SRP的批处理中断。
CBUFFER_START(UnityPerDraw)
...
float4 unity_LightmapST;
float4 unity_DynamicLightmapST;
CBUFFER_END
6. 在GI文件中修改TRANSFER_GI_DATA宏的定义,对光照贴图的UV坐标进行缩放和偏移的转换。
#define TRANSFER_GI_DATA(input, output) output.lightMapUV = input.lightMapUV * unity_LightmapST.xy + unity_LightmapST.zw;
下图展示了转换后的光照贴图UV坐标的效果。

5.2.3 采样光照贴图
1. 接下来我们可以对光照贴图进行采样了。在GI.hlsl中把源码库中的EnityLighting.hlsl文件include进来,从中获取光照贴图和它的采样器。
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/EntityLighting.hlsl"
TEXTURE2D(unity_Lightmap);
SAMPLER(samplerunity_Lightmap);
2. 创建SampleLightMap方法用来进行采样,传递光照贴图的UV坐标,如果渲染了光照贴图对象,调用SampleSingleLightmap方法对光照贴图进行采样,否则直接返回0。然后在GetGI方法中调用该函数,并将采样结果作为漫反射光照。
//采样光照贴图
float3 SampleLightMap(float2 lightMapUV)
{
#if defined(LIGHTMAP_ON)
return SampleSingleLightmap(lightMapUV);
#else
return 0.0;
#endif
}
GI GetGI(float2 lightMapUV)
{
GI gi;
gi.diffuse = SampleLightMap(lightMapUV);
return gi;
}
3. SampleSingleLightmap方法除了UV坐标还需要其它的参数,第1个参数是TEXTURE2D_ARGS宏,它需要将光照贴图和采样器作为参数;第3个参数是UV的缩放和偏移,我们之前已经用它们处理过UV坐标了,所以传入默认值即可;第4个参数是一个Bool值,它表示是否压缩了光照贴图,这是通过UNITY_LIGHTMAP_FULL_HDR是否被定义来判断的;最后一个参数是一个包含了解码指令的float4类型的变量。
return SampleSingleLightmap(TEXTURE2D_ARGS(unity_Lightmap, samplerunity_Lightmap), lightMapUV,float4(1.0, 1.0, 0.0, 0.0),
#if defined(UNITY_LIGHTMAP_FULL_HDR)
false,
#else
true,
#endif
float4(LIGHTMAP_HDR_MULTIPLIER, LIGHTMAP_HDR_EXPONENT, 0.0, 0.0));

4. 上图是采样了烘焙照明信息后的效果,烘焙照明现在非常亮,因为它还包括了天空盒的间接环境照明,在Lighting窗口的Environment Lighting中,我们把强度系数属性设置为0来禁用环境光照。

光照贴图可以大幅度提升场景渲染的真实程度,但无法作用在非静态物体上,所以看上去运动的物体和场景就显得不协调。所以我们使用光照探针(Light Probe)模拟光照贴图的效果。它的原理是:在某一光照探针的所在位置点上对光照信息进行采样,然后从该光照探针相邻的其它光照探针的位置上对光照信息进行采样,把这些采样得到的光照信息进行插值运算,便可算出这些光照探针之间某个位置的光照信息。在运行期这些插值的速度很快,可以达到实时渲染的要求。利用光照探针技术,可以避免运动的物体的光照效果和整个使用静态光照贴图的场景产生不协调的感觉。光照探针在运行时性能很高效,并且它用到的光照信息可以在运行之前快速地被预计算出来。
从实现技术角度来说,光照探针照明技术对照亮在3D空间中某一个指定点的光照信息在运行前的预计算阶段进行采样,然后把这些信息通过球谐函数(Spherical Harmonic Function,球面调和函数)进行编码打包存储。在游戏运行时,通过着色器程序可以把这些光照信息编码快速地重建出光照原始效果。Unity通过Light Probe组件实现光照探针照明技术。
类似光照贴图,光照探针也存储了场景中的照明信息,不同之处在于光照贴图存储的是光线照射到场景物体表面的照明信息,而光照探针则存储的是穿过场景中空白空间的光线信息,光照探针之间的连线表示在空间中光线的传递路径。
当然,使用光照探针照明会有一些限制,如果要处理光的高频信息,球谐函数的阶数就要增大,而当提升阶数时,所需要的性能耗费也会增加,因此Unity 3D在编码打包光照信息时用的函数都是低阶球谐函数(通常使用三阶球谐函数),即会忽略光的一些高频信息。
5.3.1 光照探针组
光照探针组件不能直接挂到一个游戏对象上面,通常需要依赖光照探针组(Light Probe Group)组件挂接,光照探针组组件可以挂到任何游戏对象上面。通过GameObject ->Light ->Light Probe Group可以创建一个光照探针组组件,默认情况下在一个立方体空间中包含六个探针,场景中用黄色小球表示的就是光照探针,它可以通过组件上的Edit Light Probes来编辑探针,你可以移动、复制或删除单个探针,就好像它们是游戏对象一样,场景中光照探针要达到一定数量才能被正确烘焙。
一个场景中可以存在多个光照探针组,Unity最终会将所有探针组合并到一起,然后创建一个连接它们的四面体体积网格,每个动态物体最终都会进入一个四面体内,对其顶点处的四个探针进行插值,从而计算并得出应用于该对象的最终照明信息。如果对象在光照探针覆盖区域外面,则改用最近的三角形代替,因此照明看起来可能有点奇怪。
默认情况下,当选择一个动态对象时,将使用一个Gizmos来显示影响该对象的光照探针,以及在其位置的插值结果。可以通过Lighting窗口中的Debug Settings条目下调整Light Probe Visualization修改该设置。


最简单的光照探针布局方式是将光照探针排列成一个规则的3D网格样式,这样的设置方式简单高效,但会消耗大量内存,因为每一个光照探针本质上是一个球形的且记录了当前采样点周围环境的纹理图像。如果一片区域的照明信息都差不多,那么就没必要使用大量光照探针了。光照探针通常用于照明效果突然改变的场合(有灯光过渡的周围),如从一个较为明亮的区域进入一个较暗的区域,且不要将它们放在被烘焙的物体里面,那样最终会变黑等等。Unity不支持所有平面化的光照探针组,即光照探针不能平坦分布在一个水平面上,光照探针之间在垂直方向上要有高度差。总而言之,放置光照探针的位置有很多讲究,需要自己去慢慢测试。
现在我们想把场景中的所有球体当成动态物体,让它们接受光照探针的照明而不是光照贴图,我们可以修改所有球体的Receive Global Illumination属性,设置为Light Probes模式即可。设置完后会发现动态物体都变成了黑色的,让我们接下来采样光照探针来获取光照信息。

5.3.2 采样光照探针
1. 光照探针的插值数据需要逐对象的传递给GPU,我们需要告诉Unity这么做,通过在CameraRenderer脚本的DrawVisibleGeometry()方法中,创建drawingSettings实例时把PerObjectData.LightProbe也赋值给perObjectData数据。
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
{
enableDynamicBatching = useDynamicBatching,
enableInstancing = useGPUInstancing,
perObjectData = PerObjectData.Lightmaps | PerObjectData.LightProbe
};
2. 我们需要在UnityInput.hlsl文件的UnityPerDraw缓冲区中定义7个float4类型变量来接收CPU传递来的光探针数据,它们是代表红色、绿色和蓝光的多项式组件,被命名为unity_SH*,其中*为A、B或C。A和B有三个版本,分别带有r,g和b后缀。
CBUFFER_START(UnityPerDraw)
...
float4 unity_SHAr;
float4 unity_SHAg;
float4 unity_SHAb;
float4 unity_SHBr;
float4 unity_SHBg;
float4 unity_SHBb;
float4 unity_SHC;
CBUFFER_END
3. 在GI.hlsl文件中创建SampleLightProbe方法对光照探针进行采样,该方法需要一个世界空间的表面属性传参。首先需要判断,若该对象正在使用光照贴图就直接返回0,否则返回0和使用SampleSH9()方法得到的光照数据之间最大值。SampleSH9方法用于采样光照探针的照明信息,它需要光照探针数据和表面的法线向量作为传参。
//光照探针采样
float3 SampleLightProbe (Surface surfaceWS)
{
#if defined(LIGHTMAP_ON)
return 0.0;
#else
float4 coefficients[7];
coefficients[0] = unity_SHAr;
coefficients[1] = unity_SHAg;
coefficients[2] = unity_SHAb;
coefficients[3] = unity_SHBr;
coefficients[4] = unity_SHBg;
coefficients[5] = unity_SHBb;
coefficients[6] = unity_SHC;
return max(0.0, SampleSH9(coefficients, surfaceWS.normal));
#endif
}
4. 给GetGI方法添加一个表面属性的传参,然后将光照探针的采样结果和光照贴图的采样结果相加得到最终的漫反射照明。
GI GetGI(float2 lightMapUV, Surface surfaceWS)
{
GI gi;
gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);
return gi;
}
5. 最后在片元函数中调用GetGI方法的时候把表面属性作为参数传递。
//获取全局照明
GI gi = GetGI(GI_FRAGMENT_DATA(input),surface);

5.3.3 光照探针代理体(LPPV)
在3D空间的一个位置点上,因为有且只使用一个球面表达式用于描述光照,所以光照探针照明还不适合用于描述光线穿过一个很大的物体时的情况,这种情况下光照会发生很多变动,从而无法精准地进行模拟。光照探针适合小物体,它的照明是基于一个点,因此不适用于大的物体。另一个限制就是,因为球谐函数是在一个球面上对光照信息进行编码,所以对于一个大型的有着平坦表面的物体,或者是一个有着凹面的物体,光照探针照明技术也不适用。如果想在一个大物体上应用光照探针照明,则需要使用光照探针代理体(Light Probe Proxy Volume)组件辅助实现。Unity在5.4版本后增加了这个光探针代理体(Light Probe Proxy Volume,LPPV)的新功能。光探针代理体是一个“解决无法直接使用光探针技术去处理的大型动态游戏对象问题”的组件。
我们做个实验,创建一个拉伸的立方体,如下图,把它的一边放在黑暗区域内,结果物体的整个颜色都会偏暗,这显然不符合照明。 我们可以使用光照探针代理体解决这个问题,只需要将Light Probe Proxy Volume组件添加到该物体上,将它Mesh Renderer组件上的的Light Probes改为Use Proxy Volume。为了让这些探针在场景视图可见,将Refresh Mode属性改为Every Frame,每帧刷新让探针可见。

5.3.4 采样LPPV
1. LPPV也需要将每个对象的数据发给GPU,在CameraRenderer脚本的DrawVisibleGeometry方法中实例化DrawingSettings的时候启用PerObjectData.LightProbeProxyVolume。
//设置渲染的shader pass和渲染排序
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
{
//设置渲染时批处理的使用状态
enableDynamicBatching = useDynamicBatching,
enableInstancing = useGPUInstancing,
perObjectData = PerObjectData.Lightmaps | PerObjectData.LightProbe | PerObjectData.LightProbeProxyVolume
};
2. 然后需要在UnityInput.hlsl的UnityPerDraw缓冲区中添加4个相关属性。
CBUFFER_START(UnityPerDraw)
...
float4 unity_ProbeVolumeParams;
float4x4 unity_ProbeVolumeWorldToObject;
float4 unity_ProbeVolumeSizeInv;
float4 unity_ProbeVolumeMin;
CBUFFER_END
3. 光照探针代理体数据会存储在一个名为unity_ProbeVolumeSH的3D float纹理中,在GI.hlsl中通过TEXTURE3D_FLOAT宏获取该纹理,并获取它的采样器。
TEXTURE3D_FLOAT(unity_ProbeVolumeSH);
SAMPLER(samplerunity_ProbeVolumeSH);
4. 在SampleLightProbe方法中通过unity_ProbeVolumeParams的X分量的值判断物体是否使用了LPPV或插值光照探针,如果使用了,必须使用SampleProbeVolumeSH4方法对光探针代理体进行采样,传参需要好几个,分别是对应纹理和采样器、世界空间的顶点位置和法线、一个转换矩阵、unity_ProbeVolumeParams的Y和Z分量,最后是unity_ProbeVolumeMin和unity_ProbeVolumeSizeInv的XYZ分量。
//光照探针采样
float3 SampleLightProbe (Surface surfaceWS)
{
#if defined(LIGHTMAP_ON)
return 0.0;
#else
//判断是否使用LPPV或插值光照探针
if (unity_ProbeVolumeParams.x)
{
return SampleProbeVolumeSH4(TEXTURE3D_ARGS(unity_ProbeVolumeSH, samplerunity_ProbeVolumeSH),surfaceWS.position, surfaceWS.normal,
unity_ProbeVolumeWorldToObject,unity_ProbeVolumeParams.y, unity_ProbeVolumeParams.z,unity_ProbeVolumeMin.xyz, unity_ProbeVolumeSizeInv.xyz);
}
else
{
float4 coefficients[7];
...
}
#endif
}

对LPPV进行采样需要对代理体的空间进行转换,以及一些其它计算,例如代理体纹理采样和球谐函数的应用等。这种情况下,仅使用L1球谐函数,结果会不太精确,但可能因单个物体的表面而异。
因为间接漫反射光照会从表面反射出来,因此还应该受到这些表面的漫反射率的影响。Unity使用一个特殊的Meta Pass来确定烘焙时从表面反射出来的光照,然后提供给烘焙系统,从而计算间接光照。目前我们没有定义该Pass,Unity会默认我们的表面为白色。
5.4.1 抽离公用属性
1. 添加新的Pass意味着需要再次定义着色器属性,我们干脆把一些公用属性抽离到一个单独的LitInput.hlsl文件中,让我们在Shaders文件夹下创建它,然后将LitPass.hlsl文件中的纹理属性和UnityPerMaterial缓冲区的属性拷贝过来,然后定义一些基本的方法获取这些属性,而不是直接访问缓冲区中的属性,代码如下:
#ifndef CUSTOM_LIT_INPUT_INCLUDED
#define CUSTOM_LIT_INPUT_INCLUDED
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
UNITY_DEFINE_INSTANCED_PROP(float, _Metallic)
UNITY_DEFINE_INSTANCED_PROP(float, _Smoothness)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
float2 TransformBaseUV(float2 baseUV)
{
float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);
return baseUV * baseST.xy + baseST.zw;
}
float4 GetBase(float2 baseUV)
{
float4 map = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, baseUV);
float4 color = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
return map * color;
}
float GetCutoff(float2 baseUV)
{
return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff);
}
float GetMetallic(float2 baseUV)
{
return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Metallic);
}
float GetSmoothness(float2 baseUV)
{
return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Smoothness);
}
#endif
2. 因为我们需要在所有的着色器Pass中访问它们,所以直接将include Common和LitInput文件的指令放到所有Pass的外面。
SubShader
{
HLSLINCLUDE
#include "../ShaderLibrary/Common.hlsl"
#include "LitInput.hlsl"
ENDHLSL
...
}
3. 将LitPass文件中重复的声明删掉。
//#include "../ShaderLibrary/Common.hlsl"
//TEXTURE2D(_BaseMap);
//SAMPLER(sampler_BaseMap);
//
//UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
////提供纹理的缩放和平移
//UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
//UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
//UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
//UNITY_DEFINE_INSTANCED_PROP(float, _Metallic)
//UNITY_DEFINE_INSTANCED_PROP(float, _Smoothness)
//UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
4. 顶点函数中我们使用定义好的TransformBaseUV方法来对UV坐标进行转换。
//计算缩放和偏移后的UV坐标
//float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);
output.baseUV = TransformBaseUV(input.baseUV);
5. 在片元函数中调整一些属性的获取方式,改为使用LitInput文件中定义的属性获取方法。
//float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
//float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
float4 base = GetBase(input.baseUV);
#if defined(_CLIPPING)
clip(base.a - GetCutoff(input.baseUV));
#endif
...
surface.metallic = GetMetallic(input.baseUV);
surface.smoothness = GetSmoothness(input.baseUV);
6. 我们还需要在ShadowCasterPass.hlsl文件中也做和LitPass.hlsl同样的修改。
7. 在Unlit.shader中我们也做类似的处理。复制LitInput.hlsl文件命名为UnlitInput.hlsl,因为这个着色器是不受光的,所以_Metallic和_Smoothness属性和相关获取方法可以删掉。然后在Unlit.shader的Subshader中做类似的include操作。最后跟之前一样,把UnlitPass.hlsl文件中重复的属性定义都删除掉。
HLSLINCLUDE
#include "../ShaderLibrary/Common.hlsl"
#include "UnlitInput.hlsl"
ENDHLSL
5.4.2 实现Meta Pass
1. 在Lit.shader和Unlit.shader中添加Meta Pass的定义,该Pass的LightMode设置为Meta,关闭剔除功能,include名为MetaPass.hlsl的文件。
Pass
{
Tags
{
"LightMode" = "Meta"
}
Cull Off
HLSLPROGRAM
#pragma target 3.5
#pragma vertex MetaPassVertex
#pragma fragment MetaPassFragment
#include "MetaPass.hlsl"
ENDHLSL
}
2. 在Shaders文件夹下新建MetaPass.hlsl文件,包含的初始代码如下。我们需要知道表面的漫反射率,所以BRDF、Surface、Shadows 和Light的库文件要include进来,我们只需要顶点对象空间的位置和基础UV,在顶点函数中裁剪空间的顶点位置设为0,片元函数通过ZERO_INITIALIZE宏把表面数据初始化为0。通过获取表面的颜色、金属度和光滑度来获取其BRDF数据。
#ifndef CUSTOM_META_PASS_INCLUDED
#define CUSTOM_META_PASS_INCLUDED
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Shadows.hlsl"
#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/BRDF.hlsl"
struct Attributes
{
float3 positionOS : POSITION;
float2 baseUV : TEXCOORD0;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 baseUV : VAR_BASE_UV;
};
Varyings MetaPassVertex (Attributes input)
{
output.positionCS = 0.0;
output.baseUV = TransformBaseUV(input.baseUV);
return output;
}
float4 MetaPassFragment (Varyings input) : SV_TARGET
{
float4 base = GetBase(input.baseUV);
Surface surface;
ZERO_INITIALIZE(Surface, surface);
surface.color = base.rgb;
surface.metallic = GetMetallic(input.baseUV);
surface.smoothness = GetSmoothness(input.baseUV);
BRDF brdf = GetBRDF(surface);
float4 meta = 0.0;
return meta;
}
#endif
现在重新烘焙场景,所有的间接照明都会消失,因为我们在片元函数中返回的是0向量,黑色表面不会反射任何东西。

3. 就像采样光照贴图一样,我们要使用光照贴图的UV坐标。不同的是,我们将UV赋值给对象空间顶点位置的XY分量,Z分量需要限制到[0,FLT_MIN]区间,其中FLT_MIN代表最小的正浮点数。然后使用TransformWorldToHClip方法将该顶点位置转换到裁剪空间中(尽管传入的不是世界空间的顶点位置,不过还是需要这么做)。
struct Attributes
{
float3 positionOS : POSITION;
float2 baseUV : TEXCOORD0;
float2 lightMapUV : TEXCOORD1;
};
Varyings MetaPassVertex(Attributes input)
{
Varyings output;
input.positionOS.xy = input.lightMapUV * unity_LightmapST.xy + unity_LightmapST.zw;
input.positionOS.z = input.positionOS.z > 0.0 ? FLT_MIN : 0.0;
output.positionCS = TransformWorldToHClip(input.positionOS);
output.baseUV = TransformBaseUV(input.baseUV);
return output;
}
5.4.3 漫反射率
1. Meta Pass可用于生成不同的数据,通过定义一个bool4类型的标记向量unity_MetaFragmentControl进行通信。在片元函数中进行判断,如果标记了X分量,则需要漫反射率。
bool4 unity_MetaFragmentControl;
float4 meta = 0.0;
if (unity_MetaFragmentControl.x)
{
meta = float4(brdf.diffuse, 1.0);
}
return meta;
2. 这足以给反射光照着色,但Unity的Meta Pass还通过加上粗糙度乘以一半的镜面反射率来提升效果。其背后的想法是,高镜面但粗糙的材质也可以传递一些间接光照。最后通过PositivePow方法扩大反射光照,但将其限制到unity_MaxOutputValue,这两个属性需要声明为float类型的。
float unity_OneOverOutputBoost;
float unity_MaxOutputValue;
meta = float4(brdf.diffuse, 1.0);
meta.rgb += brdf.specular * brdf.roughness * 0.5;
meta.rgb = min(PositivePow(meta.rgb, unity_OneOverOutputBoost), unity_MaxOutputValue);
因为我们的地面材质的颜色是红色的,所以间接光照大部分都是地面的红色。

3. 现在我们得到了正确的间接光照颜色,在Lighting.hlsl文件的GetLighting方法中,也将表面的漫反射应用上,从而得到正确的烘焙照明。
ShadowData shadowData = GetShadowData(surfaceWS);
float3 color = gi.diffuse * brdf.diffuse;

4. 还可以通过Lighting Settings中把环境光照强度设回1。然后将方向光组件的Model属性设为Mixed。现在我们又有了实时光照,并且烘焙了所有的间接漫反射光照。

有些表面可以发出光,即使场景中没用任何照明,因为它不是真正的光源,所以它不会影响其它表面,但可以参与烘焙光照贴图的计算中,从而照明周围的静态物体。
5.5.1 实现
1. 在Shader属性中添加两个属性,一个自发光纹理贴图和一个HDR自发光颜色。将颜色调成白色,自发光纹理使用Unity的自带纹理Default-Particle。
//自发光
[NoScaleOffset] _EmissionMap("Emission", 2D) = "white" {}
[HDR] _EmissionColor("Emission", Color) = (0.0, 0.0, 0.0, 0.0)

2. 在LitInput.hlsl文件的UnityPerMaterial缓冲区中添加一个自发光颜色属性,声明一个自发光纹理属性。然后定义一个GetEmission方法,将采样的自发光纹理结果和颜色值相乘得到最终的自发光颜色。
TEXTURE2D(_EmissionMap);
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
...
UNITY_DEFINE_INSTANCED_PROP(float4, _EmissionColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
float3 GetEmission (float2 baseUV)
{
float4 map = SAMPLE_TEXTURE2D(_EmissionMap, sampler_BaseMap, baseUV);
float4 color = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _EmissionColor);
return map.rgb * color.rgb;
}
3. 在LitPass.hlsl文件的片元函数末尾将自发光颜色添加到最终颜色中。
float3 color = GetLighting(surface, brdf, gi);
color += GetEmission(input.baseUV);
return float4(color, surface.alpha);
4. 还要在UnlitInput.hlsl文件中也定义一个GetEmission方法,不过只需获取基础纹理颜色即可。
float3 GetEmission (float2 baseUV)
{
return GetBase(baseUV).rgb;
}
5. 给Unlit.shader的_BaseColor基础颜色添加一个HDR的标签,使得不受光的材质也能发出非常明亮的光。
[HDR] _BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)
6. 将自发光颜色也配置在PerObjectMaterialProperties.cs脚本中,调整的颜色值同步到材质中。
static int emissionColorId = Shader.PropertyToID("_EmissionColor");
[SerializeField, ColorUsage(false, true)]
Color emissionColor = Color.black;
void OnValidate()
{
...
block.SetColor(emissionColorId, emissionColor);
GetComponent<Renderer>().SetPropertyBlock(block);
}
现在我们的场景中已经有了一些自发光的物体,后续准备进行烘焙。

5.5.2 烘焙自发光
在Meta Pass文件的片元函数中进行判断,如果unity_MetaFragmentControl的Y分量被标记,则返回自发光的颜色,Alpha为1。
if (unity_MetaFragmentControl.x)
{
...
}
else if (unity_MetaFragmentControl.y)
{
meta = float4(GetEmission(input.baseUV), 1.0);
}
现在自发光的光线还不能参与烘焙光照贴图的计算中,也就不会照亮其周围的物体。自发光是通过一个单独的Pass进行烘焙的,我们需要对每个材质进行烘焙自发光的设置才行。在CustomShaderGUI脚本中定义一个BakedEmission方法并在OnGUI中调用,在BakedEmission方法通过调用MaterialEditor的LightmapEmissionProperty方法将自发光的Global Illumination属性在材质面板中暴露出来。
Global Illumination属性有三个选项:
1. None。这是默认选项,表示物体会自发光,但自发光的颜色不会照亮其周围的物体。
2. Realtime。表示物体自发光的光线将会参与实时全局照亮计算中,这些自发光光线可以照亮周围的动态和静态物体。(已被弃用。)
3. Baked。表示本物体自发光光线将会参与烘焙光照贴图的计算中,这些自发光光线可以照亮周围的静态物体,但对动态物体无效。
我们需要对每个自发光的物体材质的Global Illumination属性设置为Baked,这将在烘焙光照贴图时使用单独的Pass来烘焙自发光。但只是这样还不能起作用,当Global Illumination的切换选项发生改变时,我们应该更新每个材质的Global Illumination Flags标志,这是一个枚举。

public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
...
this.properties = properties;
BakedEmission();
...
}
//烘焙自发光
void BakedEmission()
{
EditorGUI.BeginChangeCheck();
editor.LightmapEmissionProperty();
if (EditorGUI.EndChangeCheck())
{
foreach (Material m in editor.targets)
{
m.globalIlluminationFlags &=~MaterialGlobalIlluminationFlags.EmissiveIsBlack;
}
}
}
下图是烘焙了自发光,开启和禁用方向光的效果。


透明物体也可以进行烘焙,但需要一些额外的设置。
5.6.1 实现
1. 不幸的是,Unity的烘焙系统对透明的处理是硬编码的,首先它会根据材质的渲染队列来判断该材质是透明、不透明还是裁切材质。接着将_MainTex和_Color属性的Alpha相乘,然后通过_Cutoff属性对该透明度进行裁剪。我们的Shader中目前有定义_Cutoff属性,所以还需要定义_MainTex和_Color属性(虽然我们有定义_BaseMap和_BaseColor属性,它们作用是一样的,只是命名不一样,但是这也没办法,烘焙系统对透明的处理就是硬编码的),然后将这两个属性通过HideInInspector标签使它们不在材质面板中显示,因为我们不希望这两个属性被调节。
[HideInInspector] _MainTex("Texture for Lightmap", 2D) = "white" {}
[HideInInspector] _Color("Color for Lightmap", Color) = (0.5, 0.5, 0.5, 1.0)
2. 我们要确保_MainTex、_Color的属性值和_BaseMap、_BaseColor属性值保持一致,在CustomShaderGUI脚本中定义一个CopyLightMappingProperties方法,若_BaseMap、_BaseColor属性值有修改,则应将其同步到_MainTex和_Color中。在OnGUI方法的最后面调用该方法进行追踪。
public override void OnGUI (MaterialEditor materialEditor, MaterialProperty[] properties)
{
...
if (EditorGUI.EndChangeCheck())
{
SetShadowCasterPass();
CopyLightMappingProperties();
}
}
void CopyLightMappingProperties ()
{
MaterialProperty mainTex = FindProperty("_MainTex", properties, false);
MaterialProperty baseMap = FindProperty("_BaseMap", properties, false);
if (mainTex != null && baseMap != null)
{
mainTex.textureValue = baseMap.textureValue;
mainTex.textureScaleAndOffset = baseMap.textureScaleAndOffset;
}
MaterialProperty color = FindProperty("_Color", properties, false);
MaterialProperty baseColor =
FindProperty("_BaseColor", properties, false);
if (color != null && baseColor != null)
{
color.colorValue = baseColor.colorValue;
}
}
最后我们也对Mesh Ball脚本生成的多个对象实例来添加全局照明的支持,因为对象实例是在运行模式下生成的,因此它们无法被烘焙,但可以使用光照探针存储照明信息。
5.7.1 光照探针的支持
1. 在Mesh Ball脚本中的Graphics.DrawMeshInstanced方法调用中添加额外的5个参数来使用光照探针,第1个参数代表是否投射阴影,我们启用它。第2个布尔参数代表是否接收阴影,我们设为true。第3个参数代表层级,我们使用默认的0。第4个参数代表提供一个渲染相机,我们设置null为所有相机渲染它们。第5个参数代表光照探针插值类型,我们使用CustomProvided。
Graphics.DrawMeshInstanced(mesh,0,material,matrices,1023,block, ShadowCastingMode.On, true, 0, null, LightProbeUsage.CustomProvided);
2. 我们需要为所有对象实例生成插值光照探针,并将它们添加到材料属性块(MaterialPropertyBlock)中。这意味着在配置块时,我们需要访问实例位置。我们可以获取转换矩阵的最后一列来得到实例位置,并将它们存储在临时数组中。然后通过实例化一个SphericalHarmonicsL2类型的数组来创建每个对象实例的光照探针,并使用LightProbes.CalculateInterpolatedLightAndOcclusionProbes来填充数据,该方法需要传递三个参数,对象实例的位置和光照探针数据,第三个参数用于遮挡,我们设置为空。最后通过block.CopySHCoefficientArraysFrom方法将光照探针数据复制到材质属性块中。
if (block == null)
{
...
block.SetFloatArray(smoothnessId, smoothness);
var positions = new Vector3[1023];
for (int i = 0; i < matrices.Length; i++)
{
positions[i] = matrices[i].GetColumn(3);
}
var lightProbes = new SphericalHarmonicsL2[1023];
LightProbes.CalculateInterpolatedLightAndOcclusionProbes(positions, lightProbes, null);
block.CopySHCoefficientArraysFrom(lightProbes);
}
5.7.2.光照探针代理体的支持
也可以对对象实例添加LPPV的支持,因为实例都存在于狭小空间中,这样就不必计算和存储插值光照探针。我们添加一个LightProbeProxyVolume配置字段,如果该字段正在使用,则不需要将光照探针数据添加到材质属性块中,且将LightProbeUsage.UseProxyVolume作为DrawMeshInstanced方法的最后一个参数。
[SerializeField]
LightProbeProxyVolume lightProbeVolume = null;
void Update()
{
if (block == null)
{
...
if (!lightProbeVolume)
{
var positions = new Vector3[1023];
...
block.CopySHCoefficientArraysFrom(lightProbes);
}
block.SetFloat(cutoffId, cutoff);
}
Graphics.DrawMeshInstanced(mesh,0,material,matrices,1023,block, ShadowCastingMode.On, true, 0, null, lightProbeVolume ? LightProbeUsage.UseProxyVolume : LightProbeUsage.CustomProvided,lightProbeVolume);
}
将LPPV 组件添加到挂载Mesh Ball脚本的对象中,并将组件传递给LightProbeProxyVolume字段。可以将LPPV组件的Bounding Box Mode设置为Custom自定义代理体的世界空间区域,能够将所有对象实例包裹进来。

5|烘焙光照
提交
暂无评论